//
//  GTMNSObject+KeyValueObserving.h
//
//  Copyright 2009 Google Inc.
//
//  Licensed under the Apache License, Version 2.0 (the "License"); you may not
//  use this file except in compliance with the License.  You may obtain a copy
//  of the License at
// 
//  http://www.apache.org/licenses/LICENSE-2.0
// 
//  Unless required by applicable law or agreed to in writing, software
//  distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
//  WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
//  License for the specific language governing permissions and limitations under
//  the License.
//

//
//  MAKVONotificationCenter.m
//  MAKVONotificationCenter
//
//  Created by Michael Ash on 10/15/08.
//

// This code is based on code by Michael Ash. 
// See comment in header.

#import "GTMNSObject+KeyValueObserving.h"

#import <libkern/OSAtomic.h>
#import "GTMDefines.h"
#import "GTMDebugSelectorValidation.h"
#import "GTMObjC2Runtime.h"

// A singleton that works as a dispatch center for KVO 
// -[NSObject observeValueForKeyPath:ofObject:change:context:] and turns them
// into selector dispatches. It stores a collection of 
// GTMKeyValueObservingHelpers, and keys them via the key generated by
// -dictionaryKeyForObserver:ofObject:forKeyPath:selector.
@interface GTMKeyValueObservingCenter : NSObject {
@private
  NSMutableDictionary *observerHelpers_;
}

+ (id)defaultCenter;

- (void)addObserver:(id)observer 
             ofObject:(id)target 
         forKeyPath:(NSString *)keyPath 
           selector:(SEL)selector 
           userInfo:(id)userInfo 
            options:(NSKeyValueObservingOptions)options;
- (void)removeObserver:(id)observer 
              ofObject:(id)target 
            forKeyPath:(NSString *)keyPath 
              selector:(SEL)selector;
- (id)dictionaryKeyForObserver:(id)observer 
                      ofObject:(id)target 
                    forKeyPath:(NSString *)keyPath 
                      selector:(SEL)selector;
@end

@interface GTMKeyValueObservingHelper : NSObject {
 @private
  __weak id observer_;
  SEL selector_;
  id userInfo_;
  id target_;
  NSString* keyPath_;
}

- (id)initWithObserver:(id)observer 
                object:(id)target 
               keyPath:(NSString *)keyPath 
              selector:(SEL)selector 
              userInfo:(id)userInfo 
               options:(NSKeyValueObservingOptions)options;
- (void)deregister;

@end

@interface GTMKeyValueChangeNotification ()
- (id)initWithKeyPath:(NSString *)keyPath ofObject:(id)object 
             userInfo:(id)userInfo change:(NSDictionary *)change;
@end

@implementation GTMKeyValueObservingHelper

// For info how and why we use these statics: 
// http://lists.apple.com/archives/cocoa-dev/2006/Jul/msg01038.html
static char GTMKeyValueObservingHelperContextData;
static char* GTMKeyValueObservingHelperContext 
  = &GTMKeyValueObservingHelperContextData;

- (id)initWithObserver:(id)observer 
                object:(id)target 
               keyPath:(NSString *)keyPath 
              selector:(SEL)selector 
              userInfo:(id)userInfo
               options:(NSKeyValueObservingOptions)options {
  if((self = [super init])) {
    observer_ = observer;
    selector_ = selector;
    userInfo_ = [userInfo retain];
    
    target_ = target;
    keyPath_ = [keyPath retain];
    
    [target addObserver:self
             forKeyPath:keyPath
                options:options
                context:GTMKeyValueObservingHelperContext];
  }
  return self;
}

- (void)dealloc {
  [userInfo_ release];
  [keyPath_ release];
  [super dealloc];
}

- (void)observeValueForKeyPath:(NSString *)keyPath 
                      ofObject:(id)object 
                        change:(NSDictionary *)change 
                       context:(void *)context {
  if(context == GTMKeyValueObservingHelperContext) {
    GTMKeyValueChangeNotification *notification 
      = [[GTMKeyValueChangeNotification alloc] initWithKeyPath:keyPath
                                                      ofObject:object 
                                                      userInfo:userInfo_ 
                                                        change:change];
    [observer_ performSelector:selector_ withObject:notification];
    [notification release];
  } else {
    // COV_NF_START
    // There's no way this should ever be called.
    // If it is, the call will go up to NSObject which will assert.
    [super observeValueForKeyPath:keyPath 
                         ofObject:object 
                           change:change 
                          context:context];
    // COV_NF_END
  }
}

- (void)deregister {
  [target_ removeObserver:self forKeyPath:keyPath_];
}

@end

@implementation GTMKeyValueObservingCenter

+ (id)defaultCenter {
  static GTMKeyValueObservingCenter *center = nil;
  if(!center) {
    // do a bit of clever atomic setting to make this thread safe
    // if two threads try to set simultaneously, one will fail
    // and the other will set things up so that the failing thread
    // gets the shared center
    GTMKeyValueObservingCenter *newCenter = [[self alloc] init];
    if(!objc_atomicCompareAndSwapGlobalBarrier(nil, 
                                               newCenter, 
                                               (void *)&center)) {
      [newCenter release];  // COV_NF_LINE no guarantee we'll hit this line
    }
  }
  return center;
}

- (id)init {
  if((self = [super init])) {
    observerHelpers_ = [[NSMutableDictionary alloc] init];
  }
  return self;
}

// COV_NF_START
// Singletons don't get deallocated
- (void)dealloc {
  [observerHelpers_ release];
  [super dealloc];
}
// COV_NF_END

- (id)dictionaryKeyForObserver:(id)observer 
                      ofObject:(id)target 
                    forKeyPath:(NSString *)keyPath 
                      selector:(SEL)selector {
  NSString *key = [NSString stringWithFormat:@"%p:%p:%@:%p", 
                   observer, target, keyPath, selector];
  return key;
}

- (void)addObserver:(id)observer 
           ofObject:(id)target 
         forKeyPath:(NSString *)keyPath 
           selector:(SEL)selector
           userInfo:(id)userInfo 
            options:(NSKeyValueObservingOptions)options {
  GTMKeyValueObservingHelper *helper 
    = [[GTMKeyValueObservingHelper alloc] initWithObserver:observer 
                                                    object:target 
                                                   keyPath:keyPath 
                                                  selector:selector 
                                                  userInfo:userInfo 
                                                   options:options];
  id key = [self dictionaryKeyForObserver:observer 
                                 ofObject:target 
                               forKeyPath:keyPath 
                                 selector:selector];
  @synchronized(self) {
#if DEBUG
    GTMKeyValueObservingHelper *oldHelper = [observerHelpers_ objectForKey:key];
    if (oldHelper) {
      _GTMDevLog(@"%@ already observing %@ forKeyPath %@", 
                 observer, target, keyPath);
    }
#endif // DEBUG
    [observerHelpers_ setObject:helper forKey:key];
  }
  [helper release];
}

- (void)removeObserver:(id)observer 
              ofObject:(id)target 
            forKeyPath:(NSString *)keyPath 
              selector:(SEL)selector {
  id key = [self dictionaryKeyForObserver:observer 
                                 ofObject:target
                               forKeyPath:keyPath 
                                 selector:selector];
  GTMKeyValueObservingHelper *helper = nil;
  @synchronized(self) {
    helper = [[observerHelpers_ objectForKey:key] retain];
#if DEBUG
    if (!helper) {
      _GTMDevLog(@"%@ was not observing %@ with keypath %@", 
                 observer, target, keyPath);
    }
#endif // DEBUG
    [observerHelpers_ removeObjectForKey:key];
  }
  [helper deregister];
  [helper release];
}

@end

@implementation NSObject (GTMKeyValueObservingAdditions)

- (void)gtm_addObserver:(id)observer 
             forKeyPath:(NSString *)keyPath 
               selector:(SEL)selector 
               userInfo:(id)userInfo 
                options:(NSKeyValueObservingOptions)options {
  _GTMDevAssert(observer && [keyPath length] && selector, 
                @"Missing observer, keyPath, or selector");
  GTMKeyValueObservingCenter *center 
    = [GTMKeyValueObservingCenter defaultCenter];
  GTMAssertSelectorNilOrImplementedWithArguments(observer,
                                                 selector, 
                                                 @encode(GTMKeyValueChangeNotification *), 
                                                 NULL);
  [center addObserver:observer 
             ofObject:self 
           forKeyPath:keyPath 
             selector:selector 
             userInfo:userInfo 
              options:options];
}

- (void)gtm_removeObserver:(id)observer 
                forKeyPath:(NSString *)keyPath 
                  selector:(SEL)selector {
  _GTMDevAssert(observer && [keyPath length] && selector, 
                @"Missing observer, keyPath, or selector");
  GTMKeyValueObservingCenter *center 
    = [GTMKeyValueObservingCenter defaultCenter];
  GTMAssertSelectorNilOrImplementedWithArguments(observer,
                                                 selector, 
                                                 @encode(GTMKeyValueChangeNotification *), 
                                                 NULL);  
  [center removeObserver:observer 
                ofObject:self 
              forKeyPath:keyPath 
                selector:selector];
}
   
@end


@implementation GTMKeyValueChangeNotification

- (id)initWithKeyPath:(NSString *)keyPath ofObject:(id)object 
             userInfo:(id)userInfo change:(NSDictionary *)change {
  if ((self = [super init])) {
    keyPath_ = [keyPath copy];
    object_ = [object retain];
    userInfo_ = [userInfo retain];
    change_ = [change retain];
  }
  return self;
}

- (void)dealloc {
  [keyPath_ release];
  [object_ release];
  [userInfo_ release];
  [change_ release];
  [super dealloc];
}

- (id)copyWithZone:(NSZone *)zone {
  return [[[self class] allocWithZone:zone] initWithKeyPath:keyPath_ 
                                                   ofObject:object_
                                                   userInfo:userInfo_ 
                                                     change:change_];
}

- (BOOL)isEqual:(id)object {
  return ([keyPath_ isEqualToString:[object keyPath]]
          && [object_ isEqual:[object object]]
          && [userInfo_ isEqual:[object userInfo]]
          && [change_ isEqual:[object change]]);
}

- (NSUInteger)hash {
  return [keyPath_ hash] + [object_ hash] + [userInfo_ hash] + [change_ hash];
}

- (NSString *)keyPath {
  return keyPath_;
}

- (id)object {
  return object_;
}

- (id)userInfo {
  return userInfo_;
}

- (NSDictionary *)change {
  return change_;
}

@end
